{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 字符串数据"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 引子：进入数据的世界"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "前面我们深入学习了函数的方方面面，就像打开了一个琳琅满目的工具箱，但如果没有用武之地，再强大的工具也会变得无趣，来自现实世界里的各种数据，提供了函数们施展拳脚的空间。\n",
    "\n",
    "我们编写程序是为了让计算机帮助我们解决现实世界的问题，我们一般会对这些问题建立一个数据模型，用计算机的数据结构来表达这个模型，然后编写合适的算法（函数）来处理这个模型，最终得到我们要的结果。计算机程序千变万化，但不离其宗。从这一章开始，我们就要进入“数据”的主题，来看看程序中常用的数据模型和结构有哪些，又该如何使用它们。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你已经学习了 Python 中最基本的数据类型：整数、浮点数、布尔、字符和字符串，这些类型能很好的解决现实世界里的这几种基础数据：\n",
    "* 整数、小数；\n",
    "* 逻辑真值和假值；\n",
    "* 一般的文本。\n",
    "\n",
    "在接下去的几章，你将学到更多现实世界常见数据的表示和处理方法，比如：\n",
    "* 日期时间；\n",
    "* 电话号码、Email 等格式化文本；\n",
    "* 容纳一组数据，方便我们批量处理的**数据容器**（*data container*）；\n",
    "* 如何定制更复杂的数据结构来解决问题，比如**树型图**（*tree graph*）。\n",
    "\n",
    "同时，你也会学习其他非常重要的知识，比如**迭代器**（*iterator*）模型——这是 Python 中所有数据容器的基础。\n",
    "\n",
    "在这一章先来看看如何用字符串表示各种数据。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 一切都是字符串"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你在之前专门用了一章来学习 Python 的字符串，因为字符串实在是太重要了，和整数、浮点数、布尔等数据类型相比，字符串的应用要广泛得多也灵活得多。任何人类触及的数据，进入人眼，都可以视为一串文本，我们在看的书是文本，网页是文本，对话可以录下来再转换成文本，数据报表是文本，歌曲的乐谱和歌词都是文本，只要我们愿意，我们可以用字符串表达任何数据。\n",
    "\n",
    "要和别人交换信息时，写一个邮件或者打印一张报告，本质都是文本；当某个程序要和别的程序交换数据时，一串约定好格式的字符串是最容易处理、兼容性最好的方法。\n",
    "\n",
    "所以字符串在编程中有非常独特的地位，我们经常需要做的，就是把复杂的数据变成一个字符串，保存下来，传给别人或者别的程序；再把收到的字符串，按照约定好的格式解析出来，还原成各种数据。\n",
    "\n",
    "下面我们就来看看如何用字符串表示一些常见数据，数字、日期时间，还有电话号码、Email 等。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 字符串 <=> 数值"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "把数值类型转换为字符串很简单，用我们讲的 *f-string* 就可以轻易做到："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "n = 440312\n",
    "f = 3.1415926\n",
    "\n",
    "s1 = f\"{n}\"\n",
    "s2 = f\"{f}\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要把包含数值类型数据的字符串转换成对应的类型则借助内置函数 `int()` 和 `float()`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "440312"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "int(s1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "440312.0"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 字符串里包含的如果是整数，则不仅可以转为整数，也可以转为等值的浮点数\n",
    "float(s1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3.1415926"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 字符串里包含的如果是浮点数，则可以转为浮点数，但不能直接转为整数\n",
    "float(s2)\n",
    "# int(s2) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "注意，上面的 `int(s2)` 会导致运行时错误：`ValueError: invalid literal for int() with base 10`，因为 `'3.1415926'` 无法转换为一个整数。\n",
    "\n",
    "但下面这句是可以的，内层函数 `float(s2)` 返回是浮点数 3.1415926，外层函数相当于 `int(3.1415926)`，效果是取整，结果是整数 3："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "int(float(s2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "s3 = \"440abc312\"\n",
    "s4 = \"3.l4l5g26\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对上面两个字符串如果我们尝试调用 `int(s3)` `int(s4)` `float(s3)` `float(s4)`，也会出 `ValueError` 型的的运行时错误，因为 s3 和 s4 的内容根本不是合法的数值。正如我们讲异常处理时说的：编写程序时并不知道自己可能会处理的数据是什么，有些输入如我们所想，有些则未必，当我们收到一个字符串而认为它“应该是”某种东西时，要格外注意，如果它不是会怎样，一个好的程序应该能正确处理那些即使“不正常”的输入，得到一个合理的（*reasonable*）结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们把一个字符串转换为数值，比较安全的做法是这样的："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def is_float(s):\n",
    "    try:\n",
    "        float(s)\n",
    "        return True\n",
    "    except ValueError:\n",
    "        return False\n",
    "\n",
    "def is_int(s):\n",
    "    try:\n",
    "        int(s)\n",
    "        return True\n",
    "    except ValueError:\n",
    "        return False\n",
    "\n",
    "def parse_str(s):\n",
    "    if is_int(s):\n",
    "        print(f\"输入是整数，其值为 {int(s)}。\")\n",
    "    elif is_float(s):\n",
    "        print(f\"输入是浮点数，其值为 {float(s)}。\")\n",
    "    else:\n",
    "        print(f\"输入“{s}”不是数值类型。\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在上面我们先定义了两个辅助函数 `is_float(s)` 和 `is_int(s)`，来判断输入的字符串是不是浮点数或者整数，或者更准确的说，输入是不是可以转换为浮点数和整数，判断的方法是利用 Python 提供的“异常处理”机制，这个例子和我们介绍异常处理时举的例子很像，就是尝试用 `float()` 或者 `int()` 函数来把字符串转换为浮点数或者整数，如果有 `ValueError` 异常抛出就说明输入的字符串不能转换。\n",
    "\n",
    "随后在 `parse_str()` 函数中就用上述辅助函数先判明输入的情况，然后再做处理，这样就安全多了。我们来试下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输入是整数，其值为 440312。\n"
     ]
    }
   ],
   "source": [
    "parse_str(s1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输入是浮点数，其值为 3.1415926。\n"
     ]
    }
   ],
   "source": [
    "parse_str(s2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输入“440abc312”不是数值类型。\n"
     ]
    }
   ],
   "source": [
    "parse_str(s3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输入“3.l4l5g26”不是数值类型。\n"
     ]
    }
   ],
   "source": [
    "parse_str(s4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "将字符串解读然后转换成某种特定类型的数据，通常我们用一个词 *parse*，我们在这个文档里讲的就是把字符串 _parse_ 成各种类型数据。\n",
    "\n",
    "*Parsing a string is difficult*. 所以我们一般情况下都会用现成的、对各种情况都有周到处理的第三方模块来做，比如把字符串 *parse* 成数值可以考虑 [fastnumber](https://pypi.org/project/fastnumbers/) 这个模块，有比内置函数更好的性能和更清晰易用的接口设计。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以在命令行用 `pip install fastnumbers` 来安装这个模块，也可以直接在 Jupyter Notebook 中用 `!` 来运行这个命令（在一个新的 cell 中输入 `!pip install fastnumbers` 并运行）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "54.0"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from fastnumbers import fast_float\n",
    "\n",
    "fast_float('56.07') # => 56.07\n",
    "fast_float('bad input') # => 'bad input'\n",
    "fast_float('bad input', default=0) # => 0\n",
    "fast_float('bad input', 0) # => 0\n",
    "fast_float(54) # => 54.0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5.0"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fast_float('Ⅴ') # '\\u2164'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "7.0"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fast_float('⑦') # '\\u2466'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从上面的后两个例子可以看出，这个 `fastnumbers` 甚至可以转换各种奇怪的数字字符，随便写写的肯定做不到这种程度。\n",
    "\n",
    "如果我们必须自己写这样的代码，那就是对我们的思维严谨和周密性的考验了。有兴趣的话可以看一看 Python `int()` 方法的 [源代码](https://github.com/python/cpython/blob/master/Objects/longobject.c)，或者 fastnumber 库的 [源代码](https://github.com/SethMMorton/fastnumbers/blob/master/src/parsing.c)，不过这些源代码都是用 C 语言写的（Python 的官方解释器就是用 C 写的，所以叫 CPython）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 字符串 <=> 日期时间"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "日期时间（*datetime*）也是最经常打交道的数据类型之一，在处理和日期时间有关的问题时，我们一般会碰到这几个东西：\n",
    "* 语言定义的日期时间数据类型，通常包含年、月、日、时、分、秒等属性，不同的编程语言的实现可能有所不同，有的语言还支持 Unix 式时间戳（*Unix timestamp*），也就是用 1970 年 1 月 1 日零时整开始流逝的秒数来表示的时间（这是个著名的梗，“世界起源于 1970 年 1 月 1 日”）；\n",
    "* 用字符串表示的日期时间，比如 “2019-07-16 18:05:33”，不同国家地区对日期的表示格式是不一样的，比如我们中国习惯于“年-月-日”，美国习惯“月/日/年”，而世界上大部分其他国家都是“日/月/年”；时间的表示则有 12 小时和 24 小时制的区别，AM/PM 的不同写法等问题；还有遗留数据用两位数字表示年份带来的臭名昭著的“千年虫”问题；所以在处理字符串表示的日期时间时要非常小心地约定好表示格式；\n",
    "* 与日期时间关联的时区，如果不指明时区，时间的表示就无意义，但很多软件系统里的时间是不管时区的，一旦需要跨时区使用就会带来一大堆麻烦，比如你带着你的手机出国，手机自动切换到目的地时区，然后你拍的照片、发出的邮件都是目的地的时间，如果不做处理，当你回到常居地这些照片和邮件的时间就错了；\n",
    "* 和日历有关的一系列处理，比如今天星期几？去年第二个月有多少天、最后一天是星期几？从现在开始加上 10000 天是什么日子？通常会有一个专门的日历模块来处理这类问题。\n",
    "\n",
    "由于历史原因，日期时间的处理也有很多的坑（和梗），不过目前比较新的编程语言都提供了很完备的解决方案，我们以后会陆陆续续的碰到和学会，这里我们先重点介绍下字符串表示的日期时间数据和相关的处理。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 日期时间 => 字符串"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "# datetime 类型在 datetime 包中，使用前需要先引入\n",
    "from datetime import datetime\n",
    "\n",
    "# 使用 datetime 类型的 now() 方法来获取当前时间\n",
    "t = datetime.now()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在 datetime 类型的变量 t 里面保存了上述代码运行时的时间信息，包括年、月、日、时、分、秒、微秒和时区，我们可以方便的获取这些分量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2019-11-27 18:46:02.701232\n",
      "2019\n",
      "11\n",
      "27\n",
      "18\n",
      "46\n",
      "2\n",
      "701232\n",
      "None\n"
     ]
    }
   ],
   "source": [
    "print(t)\n",
    "print(t.year)\n",
    "print(t.month)\n",
    "print(t.day)\n",
    "print(t.hour)\n",
    "print(t.minute)\n",
    "print(t.second)\n",
    "print(t.microsecond)\n",
    "print(t.tzinfo)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到这里时区信息（*tzinfo*）输出为 `None`，因为创建 t 时我们没有提供时区，我们可以在调用 `now()` 的时候传入一个时区参数，也可以不带参数调用 `astimezone()` 方法来给时间加上操作系统设定的本地时区："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2019-11-27 18:46:02.712272+08:00\n",
      "CST\n"
     ]
    }
   ],
   "source": [
    "t = datetime.now().astimezone()\n",
    "print(t)\n",
    "print(t.tzinfo)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`datetime` 模块里不只有 `datetime` 这个类型，如果我们只关心日期可以用 `date` 类型，如果只关心时间可以使用 `time` 类型。官方有一篇[详细的文档](https://docs.python.org/3/library/datetime.html)可以参考。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面我们重点说下 `datetime` 等类型提供的 `strftime()` 方法。这个方法让我们可以用指定格式把日期时间数据转换为字符串输出，下面是一些例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "年： 2019\n",
      "月： 11\n",
      "日： 27\n",
      "完整日期： 2019-11-27\n",
      "压缩版日期： 2019-11-27\n",
      "24小时制时间： 18:46:02\n",
      "12小时制时间： 06:46:02 PM\n",
      "日期加时间： 2019-11-27 18:46:02\n",
      "日期星期加时间： 2019-11-27 Wed 18:46:02\n"
     ]
    }
   ],
   "source": [
    "# datetime 类型提供的 strftime() 方法让我们可以指定格式输出表示日期时间的字符串\n",
    "print(\"年：\", t.strftime(\"%Y\"))\n",
    "print(\"月：\", t.strftime(\"%m\"))\n",
    "print(\"日：\", t.strftime(\"%d\"))\n",
    "print(\"完整日期：\", t.strftime(\"%Y-%m-%d\"))\n",
    "print(\"压缩版日期：\", t.strftime(\"%Y-%-m-%-d\")) # 压缩格式 %-m 和 %-d 不被 Windows 支持，所以这一句在 Windows 环境下会出错\n",
    "print(\"24小时制时间：\", t.strftime(\"%H:%M:%S\"))\n",
    "print(\"12小时制时间：\", t.strftime(\"%I:%M:%S %p\"))\n",
    "print(\"日期加时间：\", t.strftime(\"%Y-%m-%d %H:%M:%S\"))\n",
    "print(\"日期星期加时间：\", t.strftime(\"%Y-%m-%d %a %H:%M:%S\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`strftime()` 需要一个参数，这个参数指定输出字符串的“时间格式”，通过上面的例子可以看到，这个格式里是各种 `%` 打头的标志，每个标志代表一个日期时间分量及其格式，有人专门做了个 [网页](http://strftime.org/) 罗列了所有可以使用的标志，可以参考。\n",
    "\n",
    "时间格式里除了 `%` 打头的标志以外都会原样输出，所以可以用我们喜欢的任何字符。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2019年11月27日 18时46分\n"
     ]
    }
   ],
   "source": [
    "print(t.strftime(\"%Y年%m月%d日 %H时%M分\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果 `strftime()` 输出的字符串只是用来展示，那么格式相对自由，规范美观就好；如果用来保存数据（比如存到数据库里），那么格式就要非常严谨，确保以后读出来的时候还能正确解析。下面我们就来看看反向操作，读出一个日期时间的字符串，可以怎么变成 `datetime` 类型的数据。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 字符串 => 日期时间"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Python 提供许多方法来构造一个日期时间类型的变量，上面看到的 `now()` 是第一类，及获取当前时间；第二类是用前面介绍过的 *Unix timestamp* 来构造一个时间，这主要是为了兼容使用 *Unix timestamp* 的系统和库；第三类就是读取和解析一个表示日期时间的字符串，我们下面介绍主要方法 `strptime()`，`strptime()` 可以看做 `strftime()` 的反向操作，使用一样的时间格式描述，我们看几个例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'datetime.datetime'> 2019-07-17 12:09:13\n"
     ]
    }
   ],
   "source": [
    "s1 = \"2019-7-17 Wed 12:09:13\"\n",
    "t1 = datetime.strptime(s1, \"%Y-%m-%d %a %H:%M:%S\")\n",
    "print(type(t1), t1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'datetime.datetime'> 2019-07-17 12:09:00\n"
     ]
    }
   ],
   "source": [
    "# 中文也没有问题\n",
    "s2 = \"2019年07月17日 12时9分\"\n",
    "t2 = datetime.strptime(s2, \"%Y年%m月%d日 %H时%M分\")\n",
    "print(type(t2), t2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在绝大多数情况下，`strptime()` 都能很好的根据时间格式描述去“套”输入的字符串，然后把里面对应的年月日时分秒取出来，然后构造出一个对应的 `datetime` 类型变量。为了确保用字符串表示的日期时间数据能够在各种数据库和程序中都被正确理解和处理，专门有一个 ISO 标准规定了大家“最好”都采用的时间格式是怎样的，这就是 [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) 标准，这一标准采用的日期时间格式描述大致是这样子的：\n",
    "\n",
    "`2019-07-17T12:38:24.091911+08:00`\n",
    "\n",
    "从左到右分三段：\n",
    "* 日期：YYYY-MM-DD，年月日之间用短横线隔开；\n",
    "* 时间：HH:MM:SS.ffffff，最多到微秒；日期和时间之间用一个大写的 'T' 作为分隔符；\n",
    "* 时区：通过与标准时的偏移量来表示时区，比如 +08:00 就是东八区，也就是我国采用的时区。\n",
    "\n",
    "除了日期，后面的部分都是可选的，可以有也可以没有。\n",
    "\n",
    "Python 通过 `isoformat()` 和 `fromisoformat()` 两个方法来支持这个标准："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = datetime.now().astimezone()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2019-11-27T18:46:02.742639+08:00\n"
     ]
    }
   ],
   "source": [
    "iso_str = t.isoformat()\n",
    "print(iso_str)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'datetime.datetime'> 2019-11-27 18:46:02.742639+08:00\n"
     ]
    }
   ],
   "source": [
    "t_from_iso_str = datetime.fromisoformat(iso_str)\n",
    "print(type(t_from_iso_str), t_from_iso_str)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "ISO 8601 标准避免了各自定义不一样的时间格式描述，如果我们处理的日期时间字符串是用于数据保存和数据交换，采用这个标准是最简单和保险的方式。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 字符串与自定义格式化数据"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "除了数值、日期时间等通用基础数据类型，我们还经常用字符串来处理和记录一些自定义的格式化数据，比如身份证号码、电话号码、Email 地址等。这些数据的特点是：\n",
    "* 有字母、数字和一些特定的符号组成，长度不会太长；\n",
    "* 有明确的格式要求；\n",
    "* 一般会在输入时检验其是否符合格式要求。\n",
    "\n",
    "所以对这类数据我们经常做的是：格式校验、搜索和替换。在处理这类数据时有个绝佳的工具“正则表达式（*regular expression*）”，正则表达式是处理文本数据的利器，学习起来有一定的门槛，所以经常吓到很多人（其中包括一些工作多年的程序员），但其实只要方法得当，学会用正则表达式处理一些常见情况并没有那么难，也是越早学会越早受益的典范。我们在附录中有一篇 [正则表达式入门](x4-regex.ipynb) 可作为学习的起点。\n",
    "\n",
    "希望系统学习正则表达式的话可以使用余晟老师编写的 [《正则指引》](https://book.douban.com/subject/30352656/) 一书，余老师还一直想做一个互动式的正则表达式学习工具，期待早日完成造福大众。另外 Python 官方文档中的 [正则表达式指引](https://docs.python.org/3/howto/regex.html) 也不错。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 手机号码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "手机号码是我们经常需要处理的数据，为了简单起见，我们限定为中国的手机号，暂不考虑国际区号，那么手机号码应该是一个 3 或 4 位的运营商段码（绝大部分是 3 位），再加 8 位号码，一共 11 或 12 位数字。具体来说运营商的号段码不是随意的，而是工信部按照规定发牌的，参考 [网友整理的信息](https://blog.csdn.net/u011415782/article/details/85601655)，目前合法的手机号码段就这些："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/china-cellphone.png\" width=\"700\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们如果不考虑新加入的两个四位的号段，也不做特别严谨的检查，手机号码的正则规则大概是这样的："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "import re"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "def is_valid_cellphone(s):\n",
    "    pattern = re.compile(r'^[1]([3-9])[0-9]{9}$')\n",
    "    if pattern.match(s):\n",
    "        return True\n",
    "    else:\n",
    "        return False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面的代码非常好懂：首先引入 Python 的正则表达式库，然后编译一个我们写好的正则规则生成 `pattern`，然后用这个 `pattern` 去匹配输入的字符串，如果匹配成功返回 `True`，否则返回 `False`。\n",
    "\n",
    "这个正则规则也很好懂：\n",
    "* `^` 表示开始，`$` 表示结束；\n",
    "* `[1]` 表示第一个字符必须是 1；\n",
    "* `([3-9])` 表示接下来是 3-9 这些数字中的一个，用小括号括起来表示这是一个 *group*，选出来我们以后可以用（比如通过号段判断运营商）；\n",
    "* `[0-9]{9}` 表示接下去是 0-9 的数字中的一个，重复 9 次。\n",
    "\n",
    "学习正则最好的办法就是看别人写好的规则，尝试去理解，如果理解不了这里有个秘笈：有不少正则规则可视化工具，可以对输入的正则规则给出可视化的解析，比如 [Regexper](https://regexper.com)，还有 [Debuggex](https://www.debuggex.com/)，都可以。\n",
    "\n",
    "现在我们可以试试给几个号码检测一下。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "is_valid_cellphone('13912345678')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "is_valid_cellphone('2345678')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果要非常精确地匹配我国现有的号段规则，这个正则规则可能就有点长了，大致是这个样子的：\n",
    "\n",
    "`^[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}$`\n",
    "\n",
    "如果把它贴到 [Regexper](https://regexper.com/#%5B1%5D%28%28%5B3%5D%5B0-9%5D%29%7C%28%5B4%5D%5B5-9%5D%29%7C%28%5B5%5D%5B0-3%2C5-9%5D%29%7C%28%5B6%5D%5B5%2C6%5D%29%7C%28%5B7%5D%5B0-8%5D%29%7C%28%5B8%5D%5B0-9%5D%29%7C%28%5B9%5D%5B1%2C8%2C9%5D%29%29%5B0-9%5D%7B8%7D) 里，可以看到结构其实也不复杂，主要都在罗列号段 *1xx* 里的各种情况，这些情况都用 `()` 分组，这样匹配完了之后我们可以知道匹配到了那种情况，方便我们根据号段来识别运营商："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/regex-visualization.svg\" width=\"500\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "手机号码的号段是个会时不时变化的规则，我们如果要编写和维护一个函数，检查手机号码是不是合法、是哪个运营商的，就要时不时更新规则。如果要处理其他国家的手机，那就更加复杂，我们把这些留给大家自己去练习。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Email"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们每个人都有电子邮件地址，基本上就是 someone@any.com 这个样子，与直觉相反，用正则表达式来检查 Email 地址是不是合法可不简单。\n",
    "\n",
    "Email 服务相关的规范相当古老，由 IETF 制订的[一系列标准](https://emailregex.com/email-validation-summary/)定义，由于历史悠久又涉及到域名规则，我们[很难写出一个完美的正则规则](https://www.regular-expressions.info/email.html)来检查 Email 地址，好在大部分时候我们不需要那么完美，一般实际应用中只要保证基本合规就可以了，实际向某个地址发送邮件之后还是要做“如果这个地址收不到信怎么办”的处理。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "幸运的是，由于 Email 校验这个问题太经典也太经常被提出来了，有人甚至专门做了个网站，叫 [Email Address Regular Expression That 99.99% Works](https://emailregex.com/)，根据这个网站提供的正则规则，我们可以写出下面的这个函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "def is_valid_email(s):\n",
    "    pattern = re.compile(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)')\n",
    "    if pattern.match(s):\n",
    "        return True\n",
    "    else:\n",
    "        return False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "is_valid_email(\"neo@cool.com\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "is_valid_email(\"neo@cool\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "利用这个函数我们可以方便的检查用户输入的是不是一个合法的 Email 地址（还记得我们之前在异常处理一课里介绍的那个例子吗）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdin",
     "output_type": "stream",
     "text": [
      "Please enter your Email:  neo.lee@gmail.com\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Your Email address is 'neo.lee@gmail.com'.\n"
     ]
    }
   ],
   "source": [
    "while True:\n",
    "    email = input('Please enter your Email: ')\n",
    "    if is_valid_email(email):\n",
    "        break\n",
    "    else:\n",
    "        print('It\\'s not a valid Email address. Please try again.')\n",
    "        \n",
    "print(f'Your Email address is \\'{email}\\'.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "互联网真是个大宝库，可以给会学习的人想要的几乎任何帮助，不是吗？"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* 字符串是用来表示各种数据的利器，重点在于约定好表示的格式，可以把数据按格式组合成字符串，也可以按照格式 *parse* 字符串得到里面的数据；\n",
    "* *Parse* 字符串时处理各种意外情况是一个关键；\n",
    "* 了解 Python 中数值类型和日期时间类型，及其与字符串之间来回转换的方法；\n",
    "* 了解用正则表达式处理特定格式字符串的基本方法。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
